package org.openwebtop.sunshine.bo;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.openwebtop.common.aop.annotation.Cache;
import org.openwebtop.common.aop.annotation.RetryOnException;
import org.openwebtop.common.aop.constant.CacheRefreshPeriod;
import org.openwebtop.sunshine.dao.CodeRepositoryDAO;
import org.openwebtop.sunshine.domain.CodeRepository;
import org.openwebtop.sunshine.subversion.SVNRepositoryFactory;
import org.openwebtop.sunshine.subversion.model.DiffResult;
import org.openwebtop.sunshine.subversion.model.FileInfo;
import org.openwebtop.sunshine.subversion.model.FileType;
import org.openwebtop.sunshine.subversion.model.RevisionInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.tmatesoft.svn.core.ISVNLogEntryHandler;
import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNLogEntryPath;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.SVNDiffClient;
import org.tmatesoft.svn.core.wc.SVNRevision;

@Component
public class SubversionBO {
	private final static long BASE_REVISION = 0;
	private final static long LASTEST_REVISION = -1;

	@Autowired
	private CodeRepositoryDAO codeRepositoryDAO;

	@Autowired
	private SVNRepositoryFactory svnRepositoryFactory;

	@RetryOnException
	@Cache(refreshPeriod = CacheRefreshPeriod.REFRESH_5_MINUTE)
	public FileType getFileType(String repositoryName, String filepath, long revision) throws Exception {
		if (StringUtils.isNotBlank(repositoryName) && StringUtils.isNotBlank(filepath)) {
			final SVNRepository repository = getSvnRepository(repositoryName);
			final SVNNodeKind kind = repository.checkPath(filepath, revision);

			if (kind == SVNNodeKind.DIR) {
				return FileType.DIRECTORY;
			} else if (kind == SVNNodeKind.FILE) {
				return FileType.FILE;
			} else if (kind == SVNNodeKind.NONE) {
				return FileType.NONE;
			}
		}

		return FileType.UNKNOWN;
	}

	@RetryOnException
	@Cache(refreshPeriod = CacheRefreshPeriod.REFRESH_5_MINUTE)
	public FileInfo getFileContents(String repositoryName, String filepath, long revision) throws Exception {
		if (StringUtils.isNotBlank(repositoryName) && StringUtils.isNotBlank(filepath)) {
			final CodeRepository codeRepository = codeRepositoryDAO.findById(repositoryName);

			if (codeRepository != null) {
				return getFileContents(repositoryName, filepath, revision, codeRepository.getEncoding());
			}
		}

		return null;
	}

	@RetryOnException
	@Cache(refreshPeriod = CacheRefreshPeriod.REFRESH_5_MINUTE)
	public FileInfo getFileContents(String repositoryName, String filepath, long revision, String encoding) throws Exception {
		if (StringUtils.isNotBlank(repositoryName) && StringUtils.isNotBlank(filepath) && StringUtils.isNotBlank(encoding)) {
			final SVNRepository repository = getSvnRepository(repositoryName);
			ByteArrayOutputStream out = null;

			try {
				out = new ByteArrayOutputStream();

				final SVNProperties properties = new SVNProperties();
				repository.getFile(filepath, revision, properties, out);

				final FileInfo fileInfo = new FileInfo();
				fileInfo.setFilepath(filepath);
				fileInfo.setAuthor(properties.getStringValue(SVNProperty.LAST_AUTHOR));
				fileInfo.setRevision(NumberUtils.toLong(properties.getStringValue(SVNProperty.REVISION)));
				fileInfo.setContents(out.toString(encoding));

				return fileInfo;
			} finally {
				IOUtils.closeQuietly(out);
			}
		}

		return null;
	}

	@RetryOnException
	@Cache(refreshPeriod = CacheRefreshPeriod.REFRESH_5_MINUTE)
	public DiffResult getDiffInfo(String repositoryName, String filepath, long startRevision, long endRevision, String encoding) throws Exception {
		if (StringUtils.isNotBlank(repositoryName) && StringUtils.isNotBlank(filepath) && StringUtils.isNotBlank(encoding)) {
			final SVNRepository repository = getSvnRepository(repositoryName);
			final SVNDiffClient diffClient = new SVNDiffClient(repository.getAuthenticationManager(), null);
			final SVNURL absoluteUrl = SVNURL.parseURIEncoded(repository.getLocation().toDecodedString() + filepath);
			ByteArrayOutputStream out = null;

			try {
				out = new ByteArrayOutputStream();
				diffClient.doDiff(absoluteUrl, SVNRevision.create(startRevision), absoluteUrl, SVNRevision.create(endRevision), SVNDepth.INFINITY, true, out);

				final DiffResult diffResult = new DiffResult();
				diffResult.setFilepath(filepath);
				diffResult.setCurrentRivision(startRevision);
				diffResult.setPreviousRevision(endRevision);
				diffResult.setDiffContents(out.toString(encoding));

				return diffResult;
			} finally {
				IOUtils.closeQuietly(out);
			}
		}

		return null;
	}

	@RetryOnException
	@Cache(refreshPeriod = CacheRefreshPeriod.REFRESH_5_MINUTE)
	public List<RevisionInfo> getRevisionInfoListToBase(String repositoryName, long revision, int size) throws Exception {
		return getRevisionInfoList(repositoryName, revision, BASE_REVISION, size);
	}

	@RetryOnException
	@Cache(refreshPeriod = CacheRefreshPeriod.REFRESH_5_MINUTE)
	public List<RevisionInfo> getRevisionInfoListFromLast(String repositoryName, long revision, int size) throws Exception {
		return getRevisionInfoList(repositoryName, LASTEST_REVISION, revision, size);
	}

	@RetryOnException
	@Cache(refreshPeriod = CacheRefreshPeriod.REFRESH_5_MINUTE)
	public List<RevisionInfo> getRevisionInfoList(String repositoryName, long fromRevision, long toRevision, int size) throws Exception {
		if (StringUtils.isNotBlank(repositoryName) && size > 0) {
			final List<RevisionInfo> revisionInfoList = new ArrayList<RevisionInfo>(size);

			final SVNRepository repository = getSvnRepository(repositoryName);
			final ISVNLogEntryHandler logEntryHandler = new ISVNLogEntryHandler() {

				@Override
				public void handleLogEntry(SVNLogEntry svnLogEntry) throws SVNException {
					final RevisionInfo revisionInfo = new RevisionInfo();
					revisionInfo.setRevision(svnLogEntry.getRevision());
					revisionInfo.setDate(svnLogEntry.getDate());
					revisionInfo.setAuthor(svnLogEntry.getAuthor());
					revisionInfo.setCommitMessage(svnLogEntry.getMessage());
					revisionInfo.setChangedPathsMap(getChangedPathsMap(repository, repository.getLocation().toDecodedString(), svnLogEntry));

					revisionInfoList.add(revisionInfo);
				}

			};

			repository.log(null, fromRevision, toRevision, true, false, size, logEntryHandler);

			return revisionInfoList;
		}

		return null;
	}

	@RetryOnException
	@Cache(refreshPeriod = CacheRefreshPeriod.REFRESH_5_MINUTE)
	@SuppressWarnings("unchecked")
	public RevisionInfo getRevisionInfo(String repositoryName, long revision) throws Exception {
		if (StringUtils.isNotBlank(repositoryName)) {
			final SVNLogEntry[] nullSvnLogEntry = new SVNLogEntry[0];

			final SVNRepository repository = getSvnRepository(repositoryName);
			final Collection<SVNLogEntry> collection = repository.log(null, null, revision, revision, true, true);
			final SVNLogEntry[] svnLogEntries = collection.toArray(nullSvnLogEntry);

			if (svnLogEntries != null && svnLogEntries.length == 1) {
				final SVNLogEntry svnLogEntry = svnLogEntries[0];

				final RevisionInfo revisionInfo = new RevisionInfo();
				revisionInfo.setRevision(svnLogEntry.getRevision());
				revisionInfo.setDate(svnLogEntry.getDate());
				revisionInfo.setAuthor(svnLogEntry.getAuthor());
				revisionInfo.setCommitMessage(svnLogEntry.getMessage());
				revisionInfo.setChangedPathsMap(getChangedPathsMap(repository, repository.getLocation().toDecodedString(), svnLogEntry));

				return revisionInfo;
			}
		}

		return null;
	}

	@RetryOnException
	@Cache(refreshPeriod = CacheRefreshPeriod.REFRESH_5_MINUTE)
	public long getLastestRevision(String repositoryName) throws Exception {
		if (StringUtils.isNotBlank(repositoryName)) {
			final List<RevisionInfo> revisionInfoList = getRevisionInfoList(repositoryName, LASTEST_REVISION, BASE_REVISION, 1);

			if (CollectionUtils.isNotEmpty(revisionInfoList)) {
				final RevisionInfo revisionInfo = revisionInfoList.get(0);

				return revisionInfo.getRevision();
			}
		}

		return LASTEST_REVISION;
	}

	@SuppressWarnings("unchecked")
	private Map<String, Character> getChangedPathsMap(SVNRepository repository, String repositoryLocation, SVNLogEntry svnLogEntry) throws SVNException {
		final String repositoryRoot = repository.getRepositoryRoot(false).toDecodedString();
		final Map<String, SVNLogEntryPath> changedPathsMap = svnLogEntry.getChangedPaths();

		if (MapUtils.isNotEmpty(changedPathsMap)) {
			final Map<String, Character> map = new HashMap<String, Character>();

			for (Entry<String, SVNLogEntryPath> entry : changedPathsMap.entrySet()) {
				final SVNLogEntryPath logEntryPath = entry.getValue();
				final String absolutePath = repositoryRoot + repository.getRepositoryPath(logEntryPath.getPath());

				if (StringUtils.startsWith(absolutePath, repository.getLocation().toDecodedString())) {
					final String relativePath = StringUtils.removeStart(absolutePath, repository.getLocation().toDecodedString() + "/");
					map.put(relativePath, logEntryPath.getType());
				}
			}

			return map;
		}

		return null;
	}

	private SVNRepository getSvnRepository(String repositoryName) throws Exception {
		if (StringUtils.isNotBlank(repositoryName)) {
			final CodeRepository codeRepository = codeRepositoryDAO.findById(repositoryName);

			if (codeRepository != null) {
				return svnRepositoryFactory.getRepository(codeRepository);
			}
		}

		throw new Exception();
	}

}
